gh-158585: Optimize bytes object creation - #158664
Conversation
|
Results of PR gh-158665 benchmark: Mean +- std dev: [ref] 34.8 ns +- 0.4 ns -> [change] 32.2 ns +- 0.5 ns: 1.08x faster. The change makes PyBytesWriter_Finish() 1.08x faster, it saves 2.6 nanoseconds. |
|
Oh. A side effect of this change is that |
|
I also ran a benchmark on PyObject *bytes = PyBytes_FromStringAndSize("abc", 3);
if (bytes == NULL) {
return NULL;
}
Py_DECREF(bytes);and: PyObject *bytes = PyBytes_FromString("abc");
if (bytes == NULL) {
return NULL;
}
Py_DECREF(bytes); |
Add bytes_alloc() helper function. bytes_alloc() and _PyBytes_FromStringAndSize() have less conditional branches than previous code, and so should be a little bit faster. * Rename existing bytes_alloc() to bytes_type_alloc(). * Replace _PyBytes_FromSize(calloc=1) with _PyBytes_FromSizeZero(). * Add set_ob_shash_unsafe(): similar to set_ob_shash() but don't use an atomic operation on Free Threading. Use this new function on newly allocated bytes objects and in bytes_resize_inplace(). * Replace PyBytes_FromStringAndSize(NULL, size) with bytes_alloc(size). * Use bytes_alloc() in PyBytes_FromString() and _PyBytes_Repeat(). * Add _PyBytes_FromStringAndSize(): similar to PyBytes_FromStringAndSize(), but str must not be NULL. * Replace PyBytes_FromStringAndSize() with _PyBytes_FromStringAndSize(). * Test that PyBytes_FromString() and PyBytes_FromStringAndSize() return singletons for 0 or 1 bytes.
Previously, it creates an empty bytes string which became the empty string singleton.
4335acb to
1a7d8d5
Compare
|
Ooops, I pushed "Add static inline _Py_NewReferenceInline() function" change by mistake. This time, I used |
|
@vstinner Auto merge? |
|
There are some corner cases that hit an assert in debug builds. Reproducer: Note: returning the singleton more often is a nice-to-have, if we return another empty bytes in some cases that would be fine for me. |
new_size cannot be 0 in practice
I consider that the PR is ready to be merged, so yes, I enabled auto merged. After I saw your comment with a regression, I disabled auto merge.
Oh, well spotted! I misread It seems like this case wasn't tested at all by the Python test suite. I fixed bug and I added tests (test_concat_cpython).
The PR only changes |
| } | ||
| } | ||
| else { | ||
| result = bytes_get_empty(); |
There was a problem hiding this comment.
What if either a or b is a subclass of bytes?
There was a problem hiding this comment.
a and b types don't matter much. What matters is what PyObject_GetBuffer() returns. If len of the two Py_buffer are zero, the function returns the empty bytes singleton. The difference with Python 3.15 is that Python 3.15 creates a fresh empty string object in this case, whereas the singleton is returned with my change.
In Python 3.15, _PyBytes_Concat() always return a bytes object. My PR doesn't change this property.
Anyway, I added more tests on a bytes subclass :-)
| self.assertIs(empty + empty_subclass, empty) | ||
| self.assertIs(empty_subclass + empty, empty) |
There was a problem hiding this comment.
| self.assertIs(empty + empty_subclass, empty) | |
| self.assertIs(empty_subclass + empty, empty) | |
| self.assertIs(empty + empty_subclass, empty) | |
| self.assertIs(empty_subclass + empty, empty) | |
| self.assertIs(empty_subclass + empty_subclass , empty) |
There was a problem hiding this comment.
Ah right, I didn't test empty_subclass + empty_subclass case. Well, I just merged my PR and I don't think that it's worth it to make a change just to add this case. It's not easy to cover all cases, but this PR extends the tested cases at least :-)
|
I merged my PR. Thanks for your review @eendebakpt! |
Add bytes_alloc() helper function. bytes_alloc() and _PyBytes_FromStringAndSize() have less conditional branches than previous code, and so should be a little bit faster.
PyBytesWriterimplementation #158585